/*
 * Copyright (c) 2012 Sean Connelly
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
 * associated documentation files (the "Software"), to deal in the Software without restriction,
 * including without limitation the rights to use, copy, modify, merge, publish, distribute,
 * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or
 * substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
 * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

var byteList = require('./byte-list.js');

function x64_create()
{
	var byl = byteList.create();

	function confirm_m8(r) // [al], [cl], [dl], [bl], [ah], [ch], [dh], [bh]
	{
		if (typeof r != 'number')
			throw 'invalid register: ' + r;
		if (r != Math.floor(r))
			throw 'invalid register: ' + r;
		if (r < 0 || r > 7)
			throw 'invalid register: ' + r;
	}

	function confirm_m16(r) // [ax], [cx], [dx], [bx], [sp], [bp], [si], [di]
	{
		confirm_m8(r);
	}

	function confirm_m32(r) // [eax], [ecx], [edx], [ebx], [esi], [edi]
	{
		if (r == 4 || r == 5) // [esp] or [ebp]
			throw 'cannot address [esp] or [ebp] without a scale+base';
		confirm_m8(r);
	}

	function confirm_m64(r) // [rax], [rcx], ... [r8], ... [r15]
	{
		if (typeof r != 'number')
			throw 'invalid register: ' + r;
		if (r != Math.floor(r))
			throw 'invalid register: ' + r;
		if (r < 0 || r > 15)
			throw 'invalid register: ' + r;
	}

	function confirm_m64e(r) // [rax], [rcx], [rdx], [rbx], [rsi], [rdi]
	{
		if (r == 4 || r == 5) // [rsp] or [rbp]
			throw 'cannot address [rsp] or [rbp] without a scale+base';
		confirm_m8(r);
	}

	function confirm_r8(r) // al, cl, dl, bl, ah, ch, dh, bh
	{
		confirm_m8(r);
	}

	function confirm_r16(r) // ax, cx, dx, bx, sp, bp, si, di
	{
		confirm_m16(r);
	}

	function confirm_r32(r) // eax, ecx, edx, ebx, esp, ebp, esi, edi
	{
		confirm_m32(r);
	}

	function confirm_r64b(r)
	{
		confirm_m64(r);
	}

	function confirm_r64w(r)
	{
		confirm_m64(r);
	}

	function confirm_r64d(r)
	{
		confirm_m64(r);
	}

	function confirm_r64(r) // rax, rcx, ... r8, ... r15
	{
		confirm_m64(r);
	}

	function confirm_r64e(r) // rax, rcx, ... rdi
	{
		confirm_m8(r);
	}

	function confirm_imm8(imm)
	{
		if (typeof imm != 'number')
			throw 'invalid immediate: ' + imm;
		if (imm != Math.floor(imm))
			throw 'invalid immediate: ' + imm;
		if (imm < 0 || imm > 0xFF)
			throw 'invalid immediate: ' + imm;
	}

	function confirm_imm16(imm)
	{
		if (typeof imm != 'number')
			throw 'invalid immediate: ' + imm;
		if (imm != Math.floor(imm))
			throw 'invalid immediate: ' + imm;
		if (imm < 0 || imm > 0xFFFF)
			throw 'invalid immediate: ' + imm;
	}

	function confirm_imm32(imm)
	{
		if (typeof imm != 'number')
			throw 'invalid immediate: ' + imm;
		if (imm != Math.floor(imm))
			throw 'invalid immediate: ' + imm;
		if (imm < 0 || imm > 0xFFFFFFFF)
			throw 'invalid immediate: ' + imm;
	}

	function confirm_imm64(imml, immh)
	{
		try {
			confirm_imm32(imml);
			confirm_imm32(immh);
		} catch (e) {
			throw 'invalid immediate: ' + imml + '/' + immh;
		}
	}

	var my = {
			clearList: function() {
					byl.clear();
					return my;
				},
			dumpList: function() {
					return byl.toString();
				},

			// call_Bm64 - illegal
			// call_Wm64 - illegal
			// call_Dm64 - illegal
			call_Qm64: {
					size: function() { return 3; },
					call: function(m1) {
							confirm_m64(m1);
							byl.pushByte(
								0x40 | (m1 >> 3),
								0xff,
								0x10 | (m1 & 0x7));
							return my;
						}
				},
			call_Qm64e: {
					size: function() { return 2; },
					call: function(m1) {
							confirm_m64e(m1);
							byl.pushByte(
								0xff,
								0x10 | m1);
							return my;
						}
				},

			cdq: {
					size: function() { return 1; },
					call: function() {
							byl.pushByte(0x99);
							return my;
						}
				},
			cdqe: {
					size: function() { return 2; },
					call: function() {
							byl.pushByte(
								0x48,
								0x98);
							return my;
						}
				},

			clc: {
					size: function() { return 1; },
					call: function() {
							byl.pushByte(0xf8);
							return my;
						}
				},
			cld: {
					size: function() { return 1; },
					call: function() {
							byl.pushByte(0xfc);
							return my;
						}
				},
			cli: {
					size: function() { return 1; },
					call: function() {
							byl.pushByte(0xfa);
							return my;
						}
				},

			cmc: {
					size: function() { return 1; },
					call: function() {
							byl.pushByte(0xf5);
							return my;
						}
				},

			cwde: {
					size: function() { return 1; },
					call: function() {
							byl.pushByte(0x98);
							return my;
						}
				},

			enter_imm16_imm8: {
					size: function() { return 4; },
					call: function(imm1, imm2) {
							confirm_imm16(imm1);
							confirm_imm8(imm2);
							byl.pushByte(
								0xc8,
								(imm1 >> 0) & 0xFF,
								(imm1 >> 8) & 0xFF,
								imm2);
							return my;
						}
				},

			fwait: {
					size: function() { return 1; },
					call: function() {
							byl.pushByte(0x9b);
							return my;
						}
				},

			hlt: {
					size: function() { return 1; },
					call: function() {
							byl.pushByte(0xf4);
							return my;
						}
				},

			in_al_dx: {
					size: function() { return 1; },
					call: function() {
							byl.pushByte(0xec);
							return my;
						}
				},
			in_eax_dx: {
					size: function() { return 1; },
					call: function() {
							byl.pushByte(0xed);
							return my;
						}
				},

			int3: {
					size: function() { return 1; },
					call: function() {
							byl.pushByte(0xcc);
							return my;
						}
				},

			int_imm8: {
					size: function() { return 2; },
					call: function(imm1) {
							confirm_imm8(imm1);
							byl.pushByte(0xcd, imm1);
							return my;
						}
				},

			iret: {
					size: function() { return 1; },
					call: function() {
							byl.pushByte(0xcf);
							return my;
						}
				},

			lahf: {
					cpuid: true, // op only valid if cpuid says so
					size: function() { return 1; },
					call: function() {
							byl.pushByte(0x9f);
							return my;
						}
				},

			leave: {
					size: function() { return 1; },
					call: function() {
							byl.pushByte(0xc9);
							return my;
						}
				},

			// mov_imm* - illegal

			// mov_?m8_* - illegal

			// mov_?m16_* - illegal

			mov_Bm32_imm8: {
					size: function() { return 4; },
					call: function(m1, imm2) {
							confirm_m32(m1);
							confirm_imm8(imm2);
							byl.pushByte(
								0x67,
								0xc6,
								m1,
								imm2 & 0xFF);
							return my;
						}
				},
			mov_Wm32_imm16: {
					size: function() { return 6; },
					call: function(m1, imm2) {
							confirm_m32(m1);
							confirm_imm16(imm2);
							byl.pushByte(
								0x67,
								0x66,
								0xc7,
								m1,
								(imm2 >> 0) & 0xFF,
								(imm2 >> 8) & 0xFF);
							return my;
						}
				},
			// mov_Dm32_imm16 - illegal
			// mov_Qm32_imm16 - illegal
			mov_Dm32_imm32: {
					size: function() {  return 7; },
					call: function(m1, imm2) {
							confirm_m32(m1);
							confirm_imm32(imm2);
							byl.pushByte(
								0x67,
								0xc7,
								m1,
								(imm2 >>  0) & 0xFF,
								(imm2 >>  8) & 0xFF,
								(imm2 >> 16) & 0xFF,
								(imm2 >> 24) & 0xFF);
							return my;
						}
				},
			mov_Qm32_imm32: {
					size: function() { return 8; },
					call: function(m1, imm2) {
							confirm_m32(m1);
							confirm_imm32(imm2);
							byl.pushByte(
								0x67,
								0x48,
								0xc7,
								m1,
								(imm2 >>  0) & 0xFF,
								(imm2 >>  8) & 0xFF,
								(imm2 >> 16) & 0xFF,
								(imm2 >> 24) & 0xFF);
							return my;
						}
				},
			// mov_Qm32_imm64
			// mov_?m32_m* - illegal
			mov_Bm32_r8: {
					size: function() { return 3; },
					call: function(m1, r2) {
							confirm_m32(m1);
							confirm_r8(r2);
							byl.pushByte(
								0x67,
								0x88,
								m1 | (r2 << 3));
							return my;
						}
				},
			// mov_Wm32_r8 - illegal
			// mov_Dm32_r8 - illegal
			// mov_Qm32_r8 - illegal
			mov_Wm32_r16: {
					size: function() { return 4; },
					call: function(m1, r2) {
							confirm_m32(m1);
							confirm_r16(r2);
							byl.pushByte(
								0x67,
								0x66,
								0x89,
								m1 | (r2 << 3));
							return my;
						}
				},
			// mov_Dm32_r16 - illegal
			// mov_Qm32_r16 - illegal
			mov_Dm32_r32: {
					size: function() { return 3; },
					call: function(m1, r2) {
							confirm_m32(m1);
							confirm_r32(r2);
							byl.pushByte(
								0x67,
								0x89,
								m1 | (r2 << 3));
							return my;
						}
				},
			// mov_Qm32_r32 - illegal
			mov_Bm32_r64b: {
					size: function() { return 4; },
					call: function(m1, r2) {
							// note: m1 is currently filtered from 0 to 7 (eax - edi), but this same
							// op sequence is used for 8 to 15 (r8d - r15d)... the high bit from the
							// register would just need to set REX.B
							confirm_m32(m1);
							confirm_r64b(r2);
							byl.pushByte(
								0x67,
								0x40 | (r2 & 0x4),
								0x88,
								m1 | ((r2 & 0x7) << 3));
							return my;
						}
				},
			// mov_Wm32_r64b - illegal
			// mov_Dm32_r64b - illegal
			// mov_Qm32_r64b - illegal
			mov_Wm32_r64w: {
					size: function() { return 5; },
					call: function(m1, r2) {
							confirm_m32(m1);
							confirm_r64w(r2);
							byl.pushByte(
								0x67,
								0x66,
								0x40 | (r2 & 0x4),
								0x89,
								m1 | ((r2 & 0x7) << 3));
							return my;
						}
				},
			// mov_Dm32_r64w - illegal
			// mov_Qm32_r64w - illegal
			mov_Dm32_r64d: {
					size: function() { return 4; },
					call: function(m1, r2) {
							confirm_m32(m1);
							confirm_r64d(r2);
							byl.pushByte(
								0x67,
								0x40 | (r2 & 0x4),
								0x89,
								m1 | ((r2 & 0x7) << 3));
							return my;
						}
				},
			// mov_Qm32_r64d - illegal
			mov_Qm32_r64: {
					size: function() { return 4; },
					call: function(m1, r2) {
							confirm_m32(m1);
							confirm_r64(r2);
							byl.pushByte(
								0x67,
								0x48 | (r2 & 0x4),
								0x89,
								m1 | ((r2 & 0x7) << 3));
							return my;
						}
				},

			// MYTODO: distinguish between Bm, Wm, Dm, Qm
			// mov_m64_imm8
			// mov_m64_imm16
			mov_Qm64_imm32: {
					size: function() { return 7; },
					call: function(m1, imm2) {
							confirm_m64(m1);
							confirm_imm32(imm2);
							byl.pushByte(
								0x48 | (m1 >> 3),
								0xc7,
								0x00 | (m1 & 0x7),
								(imm2 >>  0) & 0xFF,
								(imm2 >>  8) & 0xFF,
								(imm2 >> 16) & 0xFF,
								(imm2 >> 24) & 0xFF);
							return my;
						}
				},
			// mov_m64_imm64
			// mov_m64_m* - illegal
			// mov_m64_r8
			// mov_m64_r16
			// mov_m64_r32
			// mov_m64_r64b
			// mov_m64_r64w
			// mov_m64_r64d
			// mov_m64_r64
			mov_Qm64_r64: {
					size: function() { return 3; },
					call: function(m1, r2) {
							confirm_m64(m1);
							confirm_r64(r2);
							byl.pushByte(
								0x48 | (m1 >> 3) | ((r2 >> 3) << 2),
								0x89,
								0x00 | (m1 & 0x7) | ((r2 & 0x7) << 3));
							return my;
						}
				},

			// mov_r8_imm8
			// mov_r8_imm16
			// mov_r8_imm32
			// mov_r8_imm64
			// mov_r8_m8
			// mov_r8_m16
			// mov_r8_m32
			// mov_r8_m64
			mov_r8_r8: {
					size: function() { return 2; },
					call: function(r1, r2) {
							confirm_r8(r1);
							confirm_r8(r2);
							byl.pushByte(
								0x88,
								0xc0 | r1 | (r2 << 3));
							return my;
						}
				},
			// mov_r8_r16 - illegal
			// mov_r8_r32 - illegal
			// mov_r8_r64b
			// mov_r8_r64w - illegal
			// mov_r8_r64d - illegal
			// mov_r8_r64 - illegal

			// mov_r16_imm8
			// mov_r16_imm16
			// mov_r16_imm32
			// mov_r16_imm64
			// mov_r16_m8
			// mov_r16_m16
			// mov_r16_m32
			// mov_r16_m64
			// mov_r16_r8 - illegal
			mov_r16_r16: {
					size: function() { return 3; },
					call: function(r1, r2) {
							confirm_r16(r1);
							confirm_r16(r2);
							byl.pushByte(
								0x66,
								0x89,
								0xc0 | r1 | (r2 << 3));
							return my;
						}
				},
			// mov_r16_r32 - illegal
			// mov_r16_r64b
			// mov_r16_r64w
			// mov_r16_r64d
			// mov_r16_r64 - illegal

			// mov_r32_imm8
			// mov_r32_imm16
			// mov_r32_imm32
			// mov_r32_imm64
			// mov_r32_m8
			// mov_r32_m16
			// mov_r32_m32
			// mov_r32_m64
			// mov_r32_r8 - illegal
			// mov_r32_r16 - illegal
			mov_r32_r32: {
					size: function() { return 2; },
					call: function(r1, r2) {
							confirm_r32(r1);
							confirm_r32(r2);
							byl.pushByte(
								0x89,
								0xc0 | r1 | (r2 << 3));
							return my;
						}
				},
			// mov_r32_r64b
			// mov_r32_r64w
			// mov_r32_r64d
			// mov_r32_r64 - illegal

			// mov_r64b_imm8
			// mov_r64b_imm16
			// mov_r64b_imm32
			// mov_r64b_imm64
			// mov_r64b_m8
			// mov_r64b_m16
			// mov_r64b_m32
			// mov_r64b_m64
			// mov_r64b_r8
			// mov_r64b_r16
			// mov_r64b_r32
			// mov_r64b_r64b
			// mov_r64b_r64w
			// mov_r64b_r64d
			// mov_r64b_r64

			// mov_r64w_imm8
			// mov_r64w_imm16
			// mov_r64w_imm32
			// mov_r64w_imm64
			// mov_r64w_m8
			// mov_r64w_m16
			// mov_r64w_m32
			// mov_r64w_m64
			// mov_r64w_r8
			// mov_r64w_r16
			// mov_r64w_r32
			// mov_r64w_r64b
			// mov_r64w_r64w
			// mov_r64w_r64d
			// mov_r64w_r64

			// mov_r64d_imm8
			// mov_r64d_imm16
			// mov_r64d_imm32
			// mov_r64d_imm64
			// mov_r64d_m8
			// mov_r64d_m16
			// mov_r64d_m32
			// mov_r64d_m64
			// mov_r64d_r8
			// mov_r64d_r16
			// mov_r64d_r32
			// mov_r64d_r64b
			// mov_r64d_r64w
			// mov_r64d_r64d
			// mov_r64d_r64

			// mov_r64_imm8
			// mov_r64_imm16
			mov_r64_imm32: {
					size: function() { return 7; },
					call: function(r1, imm2) {
							confirm_r64(r1);
							confirm_imm32(imm2);
							byl.pushByte(
								0x48 | (r1 >> 3),
								0xc7,
								0xc0 | (r1 & 0x7),
								(imm2 >>  0) & 0xFF,
								(imm2 >>  8) & 0xFF,
								(imm2 >> 16) & 0xFF,
								(imm2 >> 24) & 0xFF);
							return my;
						}
				},
			mov_r64_imm64: {
					size: function() { return 10; },
					call: function(r1, imm2l, imm2h) {
							confirm_r64(r1);
							confirm_imm64(imm2l, imm2h);
							byl.pushByte(
								0x48 | (r1 >> 3),
								0xb8 | (r1 & 0x7)
								(imm2l >>  0) & 0xFF,
								(imm2l >>  8) & 0xFF,
								(imm2l >> 16) & 0xFF,
								(imm2l >> 24) & 0xFF,
								(imm2h >>  0) & 0xFF,
								(imm2h >>  8) & 0xFF,
								(imm2h >> 16) & 0xFF,
								(imm2h >> 24) & 0xFF);
							return my;
						}
 				},
			// mov_r64_m8
			// mov_r64_m16
			// mov_r64_m32
			mov_r64_m64: {
					size: function() { return 3; },
					call: function(r1, m2) {
							confirm_r64(r1);
							confirm_m64(m2);
							byl.pushByte(
								0x48 | (r1 >> 3) | ((m2 >> 3) << 2),
								0x8b,
								0x00 | (r1 & 0x7) | ((m2 & 0x7) << 3));
							return my;
						}
				},
			// mov_r64_r8
			// mov_r64_r16
			// mov_r64_r32
			// mov_r64_r64b
			// mov_r64_r64w
			// mov_r64_r64d
			mov_r64_r64: {
					size: function() { return 3; },
					call: function(r1, r2) {
							confirm_r64(r1);
							confirm_r64(r2);
							byl.pushByte(
								0x48 | (r1 >> 3) | ((r2 >> 3) << 2),
								0x89,
								0xc0 | (r1 & 0x7) | ((r2 & 0x7) << 3));
							return my;
						}
				},

			nop: {
					size: function() { return 1; },
					call: function() {
							byl.pushByte(0x90);
							return my;
						}
				},

			out_dx_al: {
					size: function() { return 1; },
					call: function() {
							byl.pushByte(0xee);
							return my;
						}
				},
			out_dx_eax: {
					size: function() { return 1; },
					call: function() {
							byl.pushByte(0xef);
							return my;
						}
				},

			pop_r64e: {
					size: function() { return 1; },
					call: function(r1) {
							confirm_r64e(r1);
							byl.pushByte(0x58 | r1);
							return my;
						}
				},

			popf: {
					size: function() { return 1; },
					call: function() {
							byl.pushByte(0x9d);
							return my;
						}
				},

			push_r64e: {
					size: function() { return 1; },
					call: function(r1) {
							confirm_r64e(r1);
							byl.pushByte(0x50 | r1);
							return my;
						}
				},

			pushf: {
					size: function() { return 1; },
					call: function() {
							byl.pushByte(0x9c);
							return my;
						}
				},

			ret: {
					size: function() { return 1; },
					call: function() {
							byl.pushByte(0xc3);
							return my;
						}
				},

			retf: {
					size: function() { return 1; },
					call: function() {
							byl.pushByte(0xcb);
							return my;
						}
				},

			sahf: {
					cpuid: true, // op only valid when cpuid says so
					size: function() { return 1; },
					call: function() {
							byl.pushByte(0x9e);
							return my;
						}
				},

			stc: {
					size: function() { return 1; },
					call: function() {
							byl.pushByte(0xf9);
							return my;
						}
				},
			std: {
					size: function() { return 1; },
					call: function() {
							byl.pushByte(0xfd);
							return my;
						}
				},
			sti: {
					size: function() { return 1; },
					call: function() {
							byl.pushByte(0xfb);
							return my;
						}
				},

			// MYTODO: bunch of other `test` instructions, similar to `mov`
			test_r64_r64: {
					size: function() { return 3; },
					call: function(r1, r2) {
							confirm_r64(r1);
							confirm_r64(r2);
							byl.pushByte(
								0x48 | (r1 >> 3) | ((r2 >> 3) << 2),
								0x85,
								0xc0 | (r1 & 0x7) | ((r2 & 0x7) << 3));
							return my;
						}
				},

			xchg_r32_eax: {
					size: function() { return 1; },
					call: function(r1) {
							confirm_r32(r1);
							byl.pushByte(0x90 | r1);
							return my;
						}
				}
		};
	return my;
}

exports.create = x64_create;
